/* 
 * File:    Client for "Tiny World of Warcraft" (TWW)
 * Authors: Jonathan Kotker (ee122-bw) and Shaan Mulchandani (ee122-bv)
 */

#define MIN(a, b) ((a) > (b) ? (b) : (a))
#define MAX(a, b) ((a) < (b) ? (b) : (a))

#include "bufferList.h"
#include "bufferListNode.h"
#include "constants.h"
#include "fileio.h"
#include "messages.h"
#include "packetAssembler.h"
#include "packetContents.h"
#include "packetParser.h"
#include "playerList.h"
#include "playerStatistics.h"
#include "playerListNode.h"
#include "stringUtils.h"

#include <arpa/inet.h>  /* for inet_ntoa() */
#include <ctype.h>      /* for isalnum() */
#include <errno.h>      /* for perror() */
#include <netinet/in.h> /* for struct sockaddr_in */
#include <signal.h>     /* for signal() */
#include <stdio.h>      /* for fprintf() */
#include <stdint.h>     /* for uint16_t, uint32_t, ... */
#include <stdlib.h>     /* for exit() */
#include <string.h>     /* for memset(), strstr() */
#include <sys/select.h> /* for select() */
#include <sys/socket.h> /* for socket() */
#include <sys/types.h>  /* for socket() */
#include <time.h>       /* for nanosleep() */
#include <unistd.h>     /* for getopt(), read(), write(), ... */



void broadcastBytes(char* bytesToSend, int len, int listener,
        bufferList* currentBuffers, playerList* currentPlayers,
        fd_set* masterFdSet, int* numConnections, int* maxFd);

void processClientPacket(packetContents* contents, int socketFd, int listener,
        bufferList* currentBuffers, playerList* currentPlayers,
        fd_set* masterFdSet, int* numConnections, int* maxFd);

/* Remove client with the given file descriptor from the arguments provided. */
void removeClient(int socketFd, int listener,
        bufferList* currentBuffers, playerList* currentPlayers,
        fd_set* masterFdSet, int* numConnections, int* maxFd) {
    // Malformed packet. Log the client off.
    FD_CLR(socketFd, masterFdSet);

    playerListNode* clientNode =
            accessPlayerFd(currentPlayers, socketFd);
    packetContents contents;

    if (clientNode != NULL) {
        // Prepare the LOGOUT_NOTIFY message to be broadcast, only if
        // clientNode is available. This is a special case for
        // invalid names.
        contents.messageType = LOGOUT_NOTIFY;
        strncpy(contents.victimName,
                clientNode->statistics->name, EE122_MAX_NAME_LENGTH);
        contents.victimName[EE122_MAX_NAME_LENGTH] = 0;

        // Remove the client from the player list.
        removePlayer(currentPlayers, clientNode->statistics->name);
    }

    // Remove the client buffer from the buffer list.
    removeBuffer(currentBuffers, socketFd);
    
    // Reduce the number of connections by one.
    (*numConnections) = (*numConnections) - 1;

    close(socketFd);

    if (clientNode != NULL) {
        // Broadcast the LOGOUT_NOTIFY message.
        char assembledPacket[300];
        int length = assemblePacket(assembledPacket, &contents);

        // Send everyone on server the LOGOUT_NOTIFY message.
        broadcastBytes(assembledPacket, length, listener,
                currentBuffers, currentPlayers, masterFdSet,
                numConnections, maxFd);
    }
};

/*
 * Sends the data in the `bytes' array. This method keeps sending until
 * all of the data has been sent, as the `send' method, by itself, does
 * not send all of the data in one go.
 */
void sendBytes(int socketFd, char *bytesToSend, int len, int listener,
        bufferList* currentBuffers, playerList* currentPlayers,
        fd_set* masterFdSet, int* numConnections, int* maxFd) {
    int bytesSent = 0, total = 0, bytesLeft = len;

    while (total < len) {
        bytesSent = send(socketFd, bytesToSend + total, bytesLeft, 0);
        if (bytesSent == -1) {
            // Connection to the client is broken.
            removeClient(socketFd, listener, currentBuffers,
                    currentPlayers, masterFdSet, numConnections, maxFd);
        }
        total += bytesSent;
        bytesLeft -= bytesSent;
    }
};

/*
 * Broadcast the given bytes to all clients.
 */
void broadcastBytes(char* bytesToSend, int len, int listener,
        bufferList* currentBuffers, playerList* currentPlayers,
        fd_set* masterFdSet, int* numConnections, int* maxFd) {
    int i;
    for (i = 0; i <= (*maxFd); i++) {
        if (FD_ISSET(i, masterFdSet) && (i != listener)
                // Also ensure that the player on the connection has logged in.
                && (accessPlayerFd(currentPlayers, i) != NULL)) {
            sendBytes(i, bytesToSend, len, listener, currentBuffers,
                    currentPlayers, masterFdSet, numConnections, maxFd);
        }
    }
};

/*
 * Process bytes coming into `socketFd'. The `currentBuffers' buffer list
 * keeps track of buffers for each active connection, and can be used for
 * collecting bytes until a complete packet is received. The `masterFdSet'
 * file descriptor set, the `playerList' player list, and the `numConnections'
 * integer is also provided so that, in the case of a broken connection,
 * the corresponding client is removed from both data structures. Finally,
 * the `listener' and the `maxFd' socket file descriptors are provided
 * for the purposes of broadcasting.
 */
void processBytes(int socketFd, int listener,
        bufferList* currentBuffers, playerList* currentPlayers,
        fd_set* masterFdSet, int* numConnections, int* maxFd) {
    // Obtain the buffer corresponding to this file descriptor.
    bufferListNode* bufferNode = accessBuffer(currentBuffers, socketFd);

    if (bufferNode == NULL) {
        fprintf(stdout, "No buffer node available for client. \n");
        return;
    }

    int done = 0;

    char* buffer = bufferNode->buffer;
    int* startPos = &(bufferNode->incomingPosition);

    int bufferSize = MAX_RECV_BUFFER_LEN - (*startPos);
    int bytesRecvd = recv(socketFd, (buffer + (*startPos)), bufferSize, 0);
    (*startPos) = (*startPos) + bytesRecvd;

    while (!done) {
        if (bytesRecvd == 0) {
            // Client has suspended connection. Log client off.
            removeClient(socketFd, listener, currentBuffers,
                    currentPlayers, masterFdSet, numConnections, maxFd);
            done = 1;
        } else if ((*startPos) < 3) {
            // We have not received enough of the header yet. Do nothing.
            done = 1;
        } else {
            int length = (buffer[1] << 8);
            length = length | buffer[2];

            if ((*startPos) < length) {
                // We have not yet received the whole packet. Do nothing.
                done = 1;
            } else {
                // We have received the whole packet. Process the packet.
                char packet[MAX_SEND_LEN];

                int i;
                for (i = 0; i < length; i++) {
                    packet[i] = buffer[i];
                }

                for (i = length; i < (*startPos); i++) {
                    buffer[i - length] = buffer[i];
                }

                *startPos = (*startPos) - length;

                if ((*startPos) >= 3) {
                    // There may be another full packet waiting to be processed.
                    // Set the `done' flag accordingly.
                    int newLength = (buffer[1] << 8);
                    newLength = newLength | buffer[2];

                    done = 0;
                    bytesRecvd = bytesRecvd - length;
                } else {
                    done = 1;
                }

                packetContents contents;
                parsePacket(packet, &contents);

                if (contents.flag == 1) {
                    // Malformed client packet. Log the client off.
                    removeClient(socketFd, listener, currentBuffers,
                            currentPlayers, masterFdSet, numConnections, maxFd);
                    break;
                }

                // Process the packet from the client.
                processClientPacket(&contents, socketFd, listener,
                        currentBuffers, currentPlayers, masterFdSet,
                        numConnections, maxFd);
            }
        }
    }
};

/*
 * Process a packet received from the client, whose contents are
 * present in `contents', through the socket file descriptor `socketFd'.
 */
void processClientPacket(packetContents* contents, int socketFd, int listener,
        bufferList* currentBuffers, playerList* currentPlayers,
        fd_set* masterFdSet, int* numConnections, int* maxFd) {

    int length;
    char assembledPacket[300];
    playerStatistics stats;

    switch (contents->messageType) {
        case LOGIN_REQUEST:
        {
            strncpy(stats.name, contents->victimName, EE122_MAX_NAME_LENGTH);

            if ((accessPlayerFd(currentPlayers, socketFd)) != NULL) {
                // Client has already logged in through the file descriptor.
                contents->errorCode = 1;
                contents->messageType = INVALID_STATE;

                length = assemblePacket(assembledPacket, contents);
                sendBytes(socketFd, assembledPacket, length, listener,
                        currentBuffers, currentPlayers, masterFdSet,
                        numConnections, maxFd);
            } else if ((accessPlayer(currentPlayers,
                    contents->victimName)) != NULL) {
                // There is a player already logged in with the same name.
                contents->errorCode = 1;
                contents->messageType = LOGIN_REPLY;

                length = assemblePacket(assembledPacket, contents);
                sendBytes(socketFd, assembledPacket, length, listener,
                        currentBuffers, currentPlayers, masterFdSet,
                        numConnections, maxFd);
            } else {                
                // The player is not already logged in.
                readfromfile(&stats);
                
                contents->hp = stats.hp;
                contents->exp = stats.exp;
                contents->xLoc = stats.xLoc;
                contents->yLoc = stats.yLoc;

                // Add the player to the player list.
                addPlayer(currentPlayers, &stats, socketFd);

                // Send the client a LOGIN_REPLY with error code of 0.
                contents->errorCode = 0;
                contents->messageType = LOGIN_REPLY;
                
                length = assemblePacket(assembledPacket, contents);
                sendBytes(socketFd, assembledPacket, length, listener,
                        currentBuffers, currentPlayers, masterFdSet,
                        numConnections, maxFd);

                // Broadcast a MOVE_NOTIFY message to every player.
                contents->messageType = MOVE_NOTIFY;

                length = assemblePacket(assembledPacket, contents);
                broadcastBytes(assembledPacket, length,
                        listener, currentBuffers, currentPlayers,
                        masterFdSet, numConnections, maxFd);
                
                // Send the new player a MOVE_NOTIFY message for each
                // of the existing players.
                char victimName[EE122_MAX_NAME_LENGTH + 1];
                strncpy(victimName, stats.name, EE122_MAX_NAME_LENGTH);

                playerListNode* currentPlayer = currentPlayers->head;
                while (currentPlayer) {
                    playerStatistics* playerStats = currentPlayer->statistics;

                    // Do not notify the victim about himself!
                    if ((strcmp(playerStats->name, victimName)) != 0) {
                        strncpy(stats.name, playerStats->name,
                                EE122_MAX_NAME_LENGTH);
                        readfromfile(&stats);

                        strncpy(contents->victimName, stats.name,
                                EE122_MAX_NAME_LENGTH);
                        contents->hp = stats.hp;
                        contents->exp = stats.exp;
                        contents->xLoc = stats.xLoc;
                        contents->yLoc = stats.yLoc;
                        
                        length = assemblePacket(assembledPacket, contents);
                        sendBytes(socketFd, assembledPacket, length, listener,
                                currentBuffers, currentPlayers, masterFdSet,
                                numConnections, maxFd);
                    }
                    currentPlayer = currentPlayer->next;
                }
            }

            break;
        }
        case MOVE:
        {
            playerListNode* sender = accessPlayerFd(currentPlayers, socketFd);

            if (sender == NULL) {
                // Player is trying to move before logging in.
                contents->errorCode = 0;
                contents->messageType = INVALID_STATE;

                length = assemblePacket(assembledPacket, contents);
                sendBytes(socketFd, assembledPacket, length, listener,
                        currentBuffers, currentPlayers, masterFdSet,
                        numConnections, maxFd);
            } else {
                strncpy(stats.name, sender->statistics->name,
                        EE122_MAX_NAME_LENGTH);
                strncpy(contents->victimName, stats.name,
                        EE122_MAX_NAME_LENGTH);

                readfromfile(&stats);
                
                if (contents->moveDir == NORTH) {
                    stats.yLoc = stats.yLoc - 3;
                    if (stats.yLoc < 0)
                        stats.yLoc += 100;
                    else if (stats.yLoc > 99)
                        stats.yLoc %= 100;
                } else if (contents->moveDir == SOUTH) {
                    stats.yLoc = stats.yLoc + 3;
                    if (stats.yLoc < 0)
                        stats.yLoc += 100;
                    else if (stats.yLoc > 99)
                        stats.yLoc %= 100;
                } else if (contents->moveDir == WEST) {
                    stats.xLoc = stats.xLoc - 3;
                    if (stats.xLoc < 0)
                        stats.xLoc += 100;
                    else if (stats.xLoc > 99)
                        stats.xLoc %= 100;
                } else if (contents->moveDir == EAST) {
                    stats.xLoc = stats.xLoc + 3;
                    if (stats.xLoc < 0)
                        stats.xLoc += 100;
                    else if (stats.xLoc > 99)
                        stats.xLoc %= 100;
                }

                time_t currTime;
                time(&currTime);
                int oldHp = stats.hp;
                stats.hp += (difftime(currTime, sender->logInTime) / 5);
                time(&(sender->logInTime));

                writetofile(&stats);

                contents->xLoc = stats.xLoc;
                contents->yLoc = stats.yLoc;
                contents->hp = stats.hp;
                contents->exp = stats.exp;
                contents->messageType = MOVE_NOTIFY;

                length = assemblePacket(assembledPacket, contents);
                // Send everyone on server the MOVE_NOTIFY message.
                broadcastBytes(assembledPacket, length, listener,
                        currentBuffers, currentPlayers, masterFdSet,
                        numConnections, maxFd);
            }
            break;
        }
        case ATTACK:
        {
            playerListNode* sender = accessPlayerFd(currentPlayers, socketFd);

            if (sender == NULL) {
                // Player is trying to attack before logging in.
                contents->errorCode = 0;
                contents->messageType = INVALID_STATE;

                length = assemblePacket(assembledPacket, contents);
                sendBytes(socketFd, assembledPacket, length, listener,
                        currentBuffers, currentPlayers, masterFdSet,
                        numConnections, maxFd);
            } else {
                strncpy(stats.name, sender->statistics->name,
                        EE122_MAX_NAME_LENGTH);
                strncpy(contents->attackerName, stats.name,
                        EE122_MAX_NAME_LENGTH);

                readfromfile(&stats);

                playerStatistics victimStats;
                strncpy(victimStats.name, contents->victimName,
                        EE122_MAX_NAME_LENGTH);
                readfromfile(&victimStats);

                if (strcmp(contents->victimName, contents->attackerName) == 0) {
                    // Do nothing.
                } else if ((accessPlayer(currentPlayers,
                        contents->victimName)) == NULL) {
                    // Do nothing.
                } else {
                    contents->damage = (rand() % 10) + 10;
                    if (contents->damage > victimStats.hp) {
                        contents->damage = victimStats.hp;
                    }

                    victimStats.hp = victimStats.hp - contents->damage;

                    // Add experience to attacker.
                    stats.exp = stats.exp + contents->damage;
                    writetofile(&stats);

                    contents->messageType = ATTACK_NOTIFY;
                    contents->hp = victimStats.hp;

                    writetofile(&victimStats);

                    length = assemblePacket(assembledPacket, contents);
                    // Send everyone on server the ATTACK_NOTIFY message.
                    broadcastBytes(assembledPacket, length, listener,
                            currentBuffers, currentPlayers, masterFdSet,
                            numConnections, maxFd);

                    if (victimStats.hp == 0) {
                        victimStats.hp = (rand() % 30) + 20;
                        contents->hp = victimStats.hp;

                        victimStats.xLoc = (rand() % 99);
                        contents->xLoc = victimStats.xLoc;
                        victimStats.yLoc = (rand() % 99);
                        contents->yLoc = victimStats.yLoc;

                        writetofile(&victimStats);

                        contents->messageType = MOVE_NOTIFY;

                        length = assemblePacket(assembledPacket, contents);
                        // Send everyone on server the MOVE_NOTIFY message.
                        broadcastBytes(assembledPacket, length, listener,
                                currentBuffers, currentPlayers, masterFdSet,
                                numConnections, maxFd);
                    }
                }
            }
            break;
        }
        case SPEAK:
        {
            playerListNode* sender = accessPlayerFd(currentPlayers, socketFd);

            if (sender == NULL) {
                // Player is trying to speak before logging in.
                contents->errorCode = 0;
                contents->messageType = INVALID_STATE;

                length = assemblePacket(assembledPacket, contents);
                sendBytes(socketFd, assembledPacket, length, listener,
                        currentBuffers, currentPlayers, masterFdSet,
                        numConnections, maxFd);
            } else {
                strncpy(stats.name, sender->statistics->name,
                        EE122_MAX_NAME_LENGTH);
                strncpy(contents->victimName, stats.name,
                        EE122_MAX_NAME_LENGTH);

                contents->messageType = SPEAK_NOTIFY;

                length = assemblePacket(assembledPacket, contents);
                // Send everyone on server the SPEAK_NOTIFY message.
                broadcastBytes(assembledPacket, length, listener,
                        currentBuffers, currentPlayers, masterFdSet,
                        numConnections, maxFd);
            }
            break;
        }
        case LOGOUT:
        {
            playerListNode* sender = accessPlayerFd(currentPlayers, socketFd);

            if (sender == NULL) {
                // Player is trying to log out before logging in.
                contents->errorCode = 0;
                contents->messageType = INVALID_STATE;

                length = assemblePacket(assembledPacket, contents);
                sendBytes(socketFd, assembledPacket, length, listener,
                        currentBuffers, currentPlayers, masterFdSet,
                        numConnections, maxFd);
            } else {
                strncpy(stats.name, sender->statistics->name,
                        EE122_MAX_NAME_LENGTH);
                strncpy(contents->victimName, stats.name,
                        EE122_MAX_NAME_LENGTH);

                removeClient(socketFd, listener, currentBuffers,
                        currentPlayers, masterFdSet, numConnections, maxFd);
            }
            break;
        }
    }
};

/*
 * Tasks to be run before exiting the program. `playerList' is the list
 * of current players, and `status' is the reason the program is exiting.
 */
void done(playerList* currentPlayers, int status) {
    clearList(currentPlayers);
    exit(status);
};

int main(int argc, char** argv) {
    // Set up variables to be used throughout the execution of the program.
    playerList currentPlayers;
    currentPlayers.head = NULL;
    bufferList currentBuffers;
    currentBuffers.head = NULL;

    if (argc != 3 || (strcmp(argv[1], "-p") != 0)) {
        // Not enough arguments.
        fprintf(stdout, "Not enough arguments available. \n");
        done(&currentPlayers, EXIT_FAILURE);
    }

    // Parse command-line arguments.
    int serverPort = atoi(argv[2]);

    // Get listener socket file descriptor.
    int listener = socket(AF_INET, SOCK_STREAM, 0);
    if (listener < 0) {
        // Unable to create connection
        fprintf(stdout, "Cannot create server. \n");
        done(&currentPlayers, EXIT_FAILURE);
    }

    // Override TCP's 2MSL constraint.
    int on = 1;
    if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (on)) < 0) {
        // Unable to create connection
        fprintf(stdout, "Cannot override 2MSL constraint. \n");
        done(&currentPlayers, EXIT_FAILURE);
    }

    // Bind to provided port.
    struct sockaddr_in sin, newSin;
    int newSinSize = sizeof (newSin);

    memset(&sin, 0, sizeof (sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;
    sin.sin_port = htons(serverPort);
    if (bind(listener, (struct sockaddr *) & sin, sizeof (sin)) < 0) {
        fprintf(stdout, message_on_server_port_bind_failure());
        done(&currentPlayers, EXIT_FAILURE);
    }

    // Listen on provided port; allow a backlog of 20 incoming connections.
    if (listen(listener, 20) < 0) {
        fprintf(stdout, "Cannot listen on provided port. \n");
        done(&currentPlayers, EXIT_FAILURE);
    }

    // Set up variables for infinite loop.
    fd_set masterReadFds; // Master file descriptor list.
    fd_set readFds; // Temporary file descriptor list.

    // Keep track of connections; maximum is 20.
    int numConnections = 0;
    // Also keep track of largest socket file descriptor.
    int maxSocketFd = 0;

    // Zero out the read file descriptor sets.
    FD_ZERO(&masterReadFds);
    FD_ZERO(&readFds);

    // Add the listener to the master set.
    FD_SET(listener, &masterReadFds);

    // Keep track of the maximum; so far, it is the listener.
    maxSocketFd = listener;

    while (1) {
        // Copy the master set into the temporary set.
        readFds = masterReadFds;
        
        if (select(maxSocketFd + 1, &readFds, NULL, NULL, NULL) < 0) {
            fprintf(stdout, "Call to select failed. \n");
            done(&currentPlayers, EXIT_FAILURE);
        }
        
        // Run through the existing connections looking for data to read.
        int i;
        for (i = 0; i <= maxSocketFd; i++) {

            if (FD_ISSET(i, &readFds)) {
                if (i == listener) {
                    if (numConnections >= MAX_CONNECTIONS) {
                        fprintf(stdout, "Reached maximum connections. \n");
                    } else {                        
                        // Listener has an incoming connection; accept it.
                        int newSocketFd = accept(listener,
                                (struct sockaddr *) & newSin, &newSinSize);

                        if (newSocketFd != -1) {
                            // Add to master set.
                            FD_SET(newSocketFd, &masterReadFds);
                            
                            // Reset maximum, if need be.
                            maxSocketFd = MAX(newSocketFd, maxSocketFd);

                            // Assign a buffer to this file descriptor.
                            addBuffer(&currentBuffers, newSocketFd);

                            numConnections++;
                        }
                    }
                } else {
                    // Deal with data from a client.
                    processBytes(i, listener, &currentBuffers,
                            &currentPlayers, &masterReadFds,
                            &numConnections, &maxSocketFd);
                }
            }
        }
    }

    done(&currentPlayers, EXIT_SUCCESS);
};
